require 'rom/http/dataset/response_transformers/schemad'
require 'rom/http/dataset/response_transformers/schemaless'

module ROM
  module HTTP
    # HTTP Dataset
    #
    # Represents a specific HTTP collection resource
    #
    # @api public
    class Dataset
      include Enumerable
      include Dry::Equalizer(:config, :options)
      include ROM::Options

      attr_reader :config

      option :projections, type: ::Array, default: [], reader: true
      option :request_method, type: ::Symbol, default: :get, reader: true
      option :path, type: ::String, default: ''
      option :params, type: ::Hash, default: {}, reader: true
      option :headers, type: ::Hash, default: {}

      class << self
        def default_request_handler(handler = Undefined)
          return @default_request_handler if Undefined === handler
          @default_request_handler = handler
        end

        def default_response_handler(handler = Undefined)
          return @default_response_handler if Undefined === handler
          @default_response_handler = handler
        end
      end


      # HTTP Dataset interface
      #
      # @param [Hash] config the Gateway's HTTP server configuration
      #
      # @param [Hash] options dataset configuration options
      # @option options [Symbol] :request_method (:get) The HTTP verb to use
      # @option options [Array]  :projections ([]) Keys to select from response tuple
      # @option options [String] :path ('') URI request path
      # @option options [Hash] :headers ({}) Additional request headers
      # @option options [Hash] :params ({}) HTTP request parameters
      #
      # @api public
      def initialize(config, options = {})
        @config = config
        @response_transformer = ResponseTransformers::Schemaless.new
        super(options)
      end

      # @overload response_transformer
      #   Return the current response transformer
      #
      # @overload response_transformer(transformer)
      #   Set a new response transformer
      #
      #   @param transformer [#call] The new response transformer
      #
      # @api private
      def response_transformer(transformer = Undefined)
        return @response_transformer if Undefined === transformer
        @response_transformer = transformer
      end

      # Return the gateway's URI
      #
      # @return [String]
      #
      # @raise [Error] if the configuration does not contain a URI
      #
      # @api public
      def uri
        config.fetch(:uri) { fail Error, ':uri configuration missing' }
      end

      # Return request headers
      #
      # Merges default headers from the Gateway configuration and the
      # current Dataset
      #
      # @example
      #   config = { Accepts: 'application/json' }
      #   users  = Dataset.new(config, headers: { 'Cache-Control': 'no-cache' }
      #   users.headers
      #   # => {:Accepts => "application/json", :'Cache-Control' => 'no-cache'}
      #
      # @return [Hash]
      #
      # @api public
      def headers
        config.fetch(:headers, {}).merge(options.fetch(:headers, {}))
      end

      # Return the dataset name
      #
      # @return [String]
      #
      # @api public
      def name
        config[:name].to_s
      end

      # Return the dataset path
      #
      # @example
      #   Dataset.new(config, path: '/users').path
      #   # => 'users'
      #
      # @return [String] the dataset path, without a leading slash
      #
      # @api public
      def path
        options[:path].to_s.sub(%r{\A/}, '')
      end

      # Return the dataset path
      #
      # @example
      #   Dataset.new(config, path: '/users').path
      #   # => '/users'
      #
      # @return [string] the dataset path, with leading slash
      #
      # @api public
      def absolute_path
        '/' + path
      end

      # Return a new dataset with given headers
      #
      # @param headers [Hash] The new headers
      #
      # @note this _replaces_ the dataset's currently configured headers.
      #   To non-destructively add a new header, use `#add_header`
      #
      # @example
      #   users = Dataset.new(config, headers: { Accept: 'application/json' })
      #   users.with_headers(:'X-Api-Key' => '1234').headers
      #   # => { :'X-Api-Key' => '1234' }
      #
      # @return [Dataset]
      #
      # @api public
      def with_headers(headers)
        __new__(config, options.merge(headers: headers))
      end

      # Return a new dataset with additional header
      #
      # @param header [Symbol] the HTTP header to add
      # @param value  [String] the header value
      #
      # @example
      #   users = Dataset.new(config, headers: { Accept: 'application/json' })
      #   users.add_header(:'X-Api-Key', '1234').headers
      #   # => { :Accept => 'application/json', :'X-Api-Key' => '1234' }
      #
      # @return [Dataset]
      #
      # @api public
      def add_header(header, value)
        with_headers(headers.merge(header => value))
      end

      # Return a new dataset with additional options
      #
      # @param opts [Hash] the new options to add
      #
      # @return [Dataset]
      #
      # @api public
      def with_options(opts)
        __new__(config, options.merge(opts))
      end

      # Add a new set projection to the result set
      #
      # @param [Array, *args] projections the keys to add to the projections
      #
      # @example
      #   users = Dataset.new(config, projections: [:login])
      #   users.project(:email, :name).projections
      #   # => [:login, :email, :name]
      #
      # @return [Dataset]
      #
      # @api public
      def project(*args)
        projections = args.first.is_a?(::Array) ? args.first : args

        with_options(
          projections: (self.projections + projections)
        )
      end

      # Return a new dataset with a different path
      #
      # @param path [String] the new request path
      #
      # @example
      #   users.with_path('/profiles').path
      #   # => 'profiles'
      #
      # @return [Dataset]
      #
      # @api public
      def with_path(path)
        with_options(path: path)
      end

      # Return a new dataset with a modified path
      #
      # @param path [String] new path fragment
      #
      # @example
      #   users.append_path('profiles').path
      #   # => users/profiles
      #
      # @return [Dataset]
      #
      # @api public
      def append_path(path)
        with_options(path: options[:path] + '/' + path)
      end

      # Return a new dataset with a different request method
      #
      # @param [Symbol] request_method the new HTTP verb
      #
      # @example
      #   users.request_method(:put)
      #
      # @return [Dataset]
      #
      # @api public
      def with_request_method(request_method)
        with_options(request_method: request_method)
      end

      # Return a new dataset with replaced request parameters
      #
      # @param [Hash] params the new request parameters
      #
      # @example
      #   users = Dataset.new(config, params: { uid: 33 })
      #   users.with_params(login: 'jdoe').params
      #   # => { :login => 'jdoe' }
      #
      # @return [Dataset]
      #
      # @api public
      def with_params(params)
        with_options(params: params)
      end

      # Iterate over each response value
      #
      # @yield [Hash] a dataset tuple
      #
      # @return [Enumerator] if no block is given
      # @return [Array<Hash>]
      #
      # @api public
      def each(&block)
        return to_enum unless block_given?
        response.each(&block)
      end

      # Perform an insert over HTTP Post
      #
      # @params [Hash] params The request parameters to send
      #
      # @return [Array<Hash>]
      #
      # @api public
      def insert(params)
        with_options(
          request_method: :post,
          params: params
        ).response
      end

      # Perform an update over HTTP Put
      #
      # @params [Hash] params The request parameters to send
      #
      # @return [Array<Hash>]
      #
      # @api public
      def update(params)
        with_options(
          request_method: :put,
          params: params
        ).response
      end

      # Perform an delete over HTTP Delete
      #
      #
      # @return [Array<Hash>]
      #
      # @api public
      def delete
        with_options(
          request_method: :delete
        ).response
      end

      # Execute the current dataset
      #
      # @return [Array<hash>]
      #
      # @api public
      def response
        response_transformer.call(
          response_handler.call(request_handler.call(self), self),
          self
        )
      end

      private

      def response_handler
        response_handler = config.fetch(:response_handler, self.class.default_response_handler)
        fail Error, ':response_handler configuration missing' if response_handler.nil?
        response_handler
      end

      def request_handler
        request_handler = config.fetch(:request_handler, self.class.default_request_handler)
        fail Error, ':response_handler configuration missing' if request_handler.nil?
        request_handler
      end

      def __new__(*args, &block)
        self.class.new(*args, &block)
      end
    end
  end
end
